-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local log = require 'mc.logging'
local consts = require 'constants'
local skynet = require 'skynet'
local json = require 'cjson'
local sender_instance = require 'tcp.sender'
local tcp_mgmt_instance = require 'tcp.tcp_mgmt'
local file_sec = require 'utils.file'
local utils_core = require 'utils.core'
local file_intf = require 'file_intf'

local module = class{}


function module:ctor(mdb_obj, name)
    self.mdb_obj = mdb_obj
    self.name = name
    self.file_list = mdb_obj.SyncFileList
    self.sync_mode = mdb_obj.SyncMode
    self.sync_type = mdb_obj.SyncType
    self.sync_interval = mdb_obj.SyncInterval
    self.sync_interval = (self.sync_interval < 5 or self.sync_interval > 60) and 5 or self.sync_interval -- 间隔范围5~60，超过时使用默认值5即可
    -- 存放文件数据，数据部分可能超出单次大小限制，因此分开存放
    self.sync_datas = {
        head = {},
        data = {}
    }
    self.sync_total_count = 0
    self.sync_failed_count = 0
    self.enabled = false  -- 发送者且使能的场景才需要同步
    self.sync_task_running = false -- 同步任务执行标记
end

function module:start_sync_task(bus)
    if self.sync_mode == consts.SYNC_MODE_REALTIME then
        self:sync_realtime()
    else
        skynet.fork_once(function ()
            self:sync_period()
        end)
    end
    log:notice('[%s] start sync, sync mode [%s]', self.name, self.sync_mode)
end

function module:initialize(bus, tcp_enabled_sig)
    tcp_enabled_sig:on(function()
        self:update_sync(bus)
    end)
    if tcp_mgmt_instance:get_instance():get_sync_role() ~= consts.DATASYNC_ROLE_SENDER then
        return
    end
    -- 开启同步任务
    self.enabled = true
    self.sync_task_running = true
    self:start_sync_task(bus)
end

function module:update_sync(bus)
    if tcp_mgmt_instance:get_instance():get_sync_role() == consts.DATASYNC_ROLE_SENDER then
        -- 已开启状态不需要更新
        if self.enabled then
            return
        end
        self.enabled = true
        -- 只有周期性同步可能出现已经停止使能但还在执行的情况
        -- 所以如果正在周期性同步，让原协程继续执行即可
        if self.sync_task_running then
            log:info('period sync task [%s] continue', self.name)
            return
        end
        -- 重启同步任务
        self:start_sync_task(bus)
        return
    end
    -- 非发送者的处理方式
    -- 已关闭状态不需要更新
    if not self.enabled then
        return
    end
    self.enabled = false
    -- 如果是实时同步需要关闭信号
    self.sync_task_running = false
    log:info('stop realtime sync [%s] task', self.name)
end

function module:sync_realtime()
    -- 添加所有监控路径
    for _, path in pairs(self.file_list) do
        file_intf.watch_directory(path)
    end
    local change_list
    skynet.fork_once(function ()
        while self.enabled do
            skynet.sleep(500)  -- 每5秒检测一次(500)
            change_list = file_intf.get_changes()
            if not change_list or not next(change_list) then
                goto continue
            end
            self:read_files(change_list)
            ::continue::
        end
        self.sync_task_running = false
        file_intf.stop_and_clear()
        log:info('stop period sync task [%s]', self.name)
    end)
end

local function get_file_type(path)
    -- 先校验软连接，is_file无法区分
    local _, _, code = os.execute('test -L "' .. path .. '"')
    if code == 0 then
        return 'File_Link'
    elseif utils_core.is_file(path) then
        return 'File'
    elseif utils_core.is_dir(path) then
        return 'Dir'
    end
    return nil
end

local function get_permissions(path)
    local perm_handle = io.popen('stat -c %a ' .. path)
    local permissions = perm_handle:read('*a'):match('^%s*(.-)%s*$')
    perm_handle:close()
    return tonumber(permissions)
end

local function process_file(path)
    local content, permissions
    local ok, res = pcall(function()
        local file, file_err = file_sec.open_s(path, 'rb')
        if not file then
            log:info('can not open file [%s][%s]', path, file_err)
            return false
        end
        content = file:read('*all')
        file:close()
        permissions = get_permissions(path)
        return true
    end)
    -- 其他异常
    if not ok then
        log:info('open file error [%s]', res)
        return false
    end
    -- 文件打开失败
    if not res then
        return false
    end
    local head = {
        name = path:match('([^/\\]+)$'),
        path = path,
        permissions = permissions,
        type = 'File'
    }
    return head, content
end

-- 返回软连接的目标文件
local function process_file_link(path)
    local target
    local ok, err = pcall(function ()
        local command = 'readlink "' .. path .. '"'
        local handle = io.popen(command)
        target = handle:read('*a'):match('^%s*(.-)%s*$')
        handle:close()
        return nil
    end)
    if not ok or target == '' then
        log:info('open file link [%s] failed [%s]', path, err)
        return nil
    end
    local head = {
        type = 'File_Link',
        path = path
    }
    return head, target
end

local function traverse_directory(path)
    local dir_info = {path = path, permissions = get_permissions(path), subdirs = {}}
    local command = 'ls -A "' .. path .. '"'
    local handle = io.popen(command)
    local list = handle:read('*a')
    handle:close()
    local sub_path, sub_permissions, subdir_info
    for dir in list:gmatch('([^\n]+)') do
        sub_path = path .. '/' .. dir
        -- 检查路径是否为目录
        if utils_core.is_dir(sub_path) then
            subdir_info = traverse_directory(sub_path)  -- 收集子目录
            sub_permissions = get_permissions(sub_path)
            subdir_info.permissions = sub_permissions
            table.insert(dir_info.subdirs, subdir_info)
        end
    end
    return dir_info
end

local function process_dir(path)
    local result = traverse_directory(path)
    local head = {
        type = 'Dir',
        path = path
    }
    return head, result
end

local process_list = {
    ['File'] = process_file,
    ['Dir'] = process_dir,
    ['File_Link'] = process_file_link
}

function module:read_file(path)
    local file_type = get_file_type(path)
    -- 可能还未创建目标文件
    if not file_type or not process_list[file_type] then
        log:info('invalid file type of [%s]', path)
        return false
    end
    self.sync_datas.head[path] ,self.sync_datas.data[path] = process_list[file_type](path)
    return true
end

function module:sync_period()
    local interval = self.sync_interval * consts.SKYNET_ONE_SECOND
    while self.enabled do
        skynet.sleep(interval)
        self:read_files(self.file_list)
    end
    self.sync_task_running = false
    log:info('stop period sync [%s] task', self.name)
end

local function split_data(data)
    local chunks = {}
    local chunk
    for i = 1, #data, consts.DATASYNC_FILE_SIZE do
        chunk = data:sub(i, i + consts.DATASYNC_FILE_SIZE - 1)
        table.insert(chunks, chunk)
    end
    return chunks, #chunks
end

function module:read_files(list)
    local result = true
    for _, path in pairs(list) do
        if not self:read_file(path) then
            result = false
        end
    end
    -- 只要有一个文件同步异常就视作此次同步异常
    if not result then
        self.sync_failed_count = self.sync_failed_count + 1
    end
    self:send_sync_item()
    -- 同步完一次就清空避免传输旧数据
    self.sync_datas = {
        head = {},
        data = {}
    }
end

-- 封装信息
function module:send_sync_item()
    self.sync_total_count = self.sync_total_count + 1
    -- 预期是要有数据的，因此算作一次同步
    if not next(self.sync_datas.head) then
        log:info('not exist sync datas')
        return
    end
    -- 先传输头文件
    local msg = {
        SrcName = self.name,
        Type = 'Head',
        Data = self.sync_datas.head
    }
    sender_instance.get_instance():send_msg(msg, 'file')
    -- 分批传输数据
    msg.Type = 'Data'
    msg.End = false
    local datas = json.encode(self.sync_datas.data)
    if #datas > consts.DATASYNC_FILE_SIZE then
        local chunks, length = split_data(datas)
        for i = 1, length, 1 do
            msg.Data = chunks[i]
            if i == length then
                msg.End = true
            end
            sender_instance.get_instance():send_msg(msg, 'file')
        end
    else
        msg.End = true
        msg.Data = datas
        sender_instance.get_instance():send_msg(msg, 'file')
    end
end

return module